package com.zzhua.test;

import cn.hutool.core.convert.Convert;
import cn.hutool.core.io.FileUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.bytedeco.ffmpeg.avcodec.AVPacket;
import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.ffmpeg.global.avutil;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.FFmpegFrameRecorder;
import org.bytedeco.javacv.FFmpegLogCallback;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.FrameRecorder;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.UUID;

/**
 * javacv ffmpeg 工具类
 */
@Slf4j
public class FfmpegUtil {

    /**
     * mp4, m3u8
     *
     * @param filePathName       需要转换文件
     * @param toFilePath 需要转换的文件路径
     */
    public static String mp4ToM3u8(String filePathName, String toFilePath) throws Exception {

        avutil.av_log_set_level(avutil.AV_LOG_INFO);
        FFmpegLogCallback.set();

        boolean isStart = true;// 该变量建议设置为全局控制变量，用于控制录制结束
        //加载文件
        FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(filePathName);
        //grabber.setAudioChannels(1);
        grabber.start();
        String fileName = UUID.randomUUID().toString().replaceAll("-", "");
        File tempFile3 = new File(toFilePath, fileName + ".m3u8");

        String prefixName = toFilePath + fileName;
        //生成加密key
        String secureFileName = prefixName + ".key";
        byte[] secureRandom = getSecureRandom();
        FileUtil.writeBytes(secureRandom,secureFileName);

        String toHex = Convert.toHex(secureRandom);
        String keyInfoPath = toFilePath +"key.keyinfo";
        //写入加密文件
        writeKeyInfo(keyInfoPath,fileName + ".key",secureFileName,toHex);

        FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(tempFile3, grabber.getImageWidth(), grabber.getImageHeight(), grabber.getAudioChannels());
        //格式方式
        recorder.setFormat("hls");
        //关于hls_wrap的说明，hls_wrap表示重复覆盖之前ts切片，这是一个过时配置，ffmpeg官方推荐使用hls_list_size 和hls_flags delete_segments代替hls_wrap
        //设置单个ts切片的时间长度（以秒为单位）。默认值为2秒
        recorder.setOption("hls_time", "10");
        //不根据gop间隔进行切片,强制使用hls_time时间进行切割ts分片
        recorder.setOption("hls_flags", "split_by_time");

        //设置播放列表条目的最大数量。如果设置为0，则列表文件将包含所有片段，默认值为5
        // 当切片的时间不受控制时，切片数量太小，就会有卡顿的现象
        recorder.setOption("hls_list_size", "0");
        //自动删除切片，如果切片数量大于hls_list_size的数量，则会开始自动删除之前的ts切片，只保留hls_list_size个数量的切片
        recorder.setOption("hls_flags", "delete_segments");
        //ts切片自动删除阈值，默认值为1，表示早于hls_list_size+1的切片将被删除
        recorder.setOption("hls_delete_threshold", "1");
        /*hls的切片类型：
         * 'mpegts'：以MPEG-2传输流格式输出ts切片文件，可以与所有HLS版本兼容。
         * 'fmp4':以Fragmented MP4(简称：fmp4)格式输出切片文件，类似于MPEG-DASH，fmp4文件可用于HLS version 7和更高版本。
         */
        recorder.setOption("hls_segment_type", "mpegts");
        //指定ts切片生成名称规则，按数字序号生成切片,例如'file%03d.ts'，就会生成file000.ts，file001.ts，file002.ts等切片文件
        //recorder.setOption("hls_segment_filename", toFilePath + "-%03d.ts");
        recorder.setOption("hls_segment_filename", toFilePath + File.separator + fileName + "-%5d.ts");
        //加密
        recorder.setOption("hls_key_info_file", keyInfoPath);
        // 设置第一个切片的编号
//        recorder.setOption("start_number", String.valueOf(tsCont));
//        recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P);

        // 转码
        log.info("{} | 启动Hls转码录制器……", toFilePath);
        //      设置零延迟
        //recorder.setVideoOption("tune", "zerolatency");
        recorder.setVideoOption("tune", "fastdecode");
        // 快速
        recorder.setVideoOption("preset", "ultrafast");
//        recorder.setVideoOption("crf", "26");
        recorder.setVideoOption("threads", "12");
        recorder.setVideoOption("vsync", "2");
        recorder.setFrameRate(grabber.getFrameRate());// 设置帧率
//        recorder.setGopSize(25);// 设置gop,与帧率相同，相当于间隔1秒chan's一个关键帧
//		recorder.setVideoBitrate(100 * 1000);// 码率500kb/s
        recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
//        recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P);
        recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC);


        //如果想截取规定时间段视频
//        recorder.start();
//        Frame frame;
//        while ((frame = grabber.grabImage()) != null) {
//            try {
//                recorder.record(frame);
//            } catch (FrameRecorder.Exception e) {
//                log.error("转码异常：{}", e);
//            }
//        }
        recorder.start(grabber.getFormatContext());
        AVPacket packet;
        while ((packet = grabber.grabPacket()) != null) {
            try {
                recorder.recordPacket(packet);
            } catch (FrameRecorder.Exception e) {
                log.error("转码异常：{}", e);
            }
        }
        recorder.setTimestamp(grabber.getTimestamp());
        recorder.stop();
        recorder.release();
        grabber.stop();
        grabber.release();

        File dest = new File(filePathName);
        if (dest.isFile() && dest.exists()) {
            dest.delete();
            log.warn("临时文件 {}已删除", dest.getName());
        }
        log.info("转码m3u8：{}", tempFile3.getAbsolutePath());
        return tempFile3.getAbsolutePath();
    }

    /**
     * 安全安全随机
     *
     * @return {@link byte[]}
     */
    public static byte[] getSecureRandom(){
        byte[] bytes = new byte[16];
        new SecureRandom().nextBytes(bytes);
        return bytes;
    }

    /**
     * 写入关键文件数据
     *
     * @param keyInfoPath 路径
     * @param decrypt    解密
     * @param encrypt    加密
     */
    public static void writeKeyInfo(String keyInfoPath,String decrypt,String encrypt,String IV) throws IOException {
        try (BufferedWriter writer = new BufferedWriter(new FileWriter(keyInfoPath));){
            writer.write(decrypt);
            writer.newLine();
            writer.write(encrypt);
            writer.newLine();
            if(StringUtils.isNotBlank(IV)){
                writer.write(IV);
            }
            writer.flush();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

